/*
 * University of British Columbia
 * Department of Computer Science
 * CPSC317 - Internet Programming
 * Assignment 1
 * 
 * Author: 
 * January 2012
 * 
 * This code may not be used without written consent of the authors, except for 
 * current and future projects and assignments of the CPSC317 course at UBC.
 */

package ubc.cs317.xmpp.net;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.UnknownHostException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.TextOutputCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslClient;
import javax.security.sasl.SaslException;
import javax.xml.bind.DatatypeConverter;

import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import ubc.cs317.xmpp.exception.XMPPException;
import ubc.cs317.xmpp.model.Contact;
import ubc.cs317.xmpp.model.ContactStatus;
import ubc.cs317.xmpp.model.Message;
import ubc.cs317.xmpp.model.Session;

/**
 * This class describes the XMPP connection handler. A socket connection is
 * created when an instance of this handler is created, and methods are provided
 * for most common operations in XMPP.
 * 
 * This class will not in any case make a direct reference to any class or
 * method that represents a specific UI library.
 */
public class XMPPConnection {

	/**
	 * Default TCP port for client-server communication in XMPP.
	 */
	public static final int XMPP_DEFAULT_PORT = 5222;

	/**
	 * Session object for communication between the network component and the
	 * chat model and UI.
	 */
	private Session session;

	/**
	 * Socket object associated to the communication between this client and the
	 * XMPP server.
	 */
	private Socket socket;

	/**
	 * XMPP reader helper, used to obtain XML nodes from the XMPP stream.
	 */
	private XMPPStreamReader xmppReader;

	/**
	 * XMPP writer helper, used to write XML nodes to the XMPP stream.
	 */
	private XMPPStreamWriter xmppWriter;

	/**
	 * XMPP Features element!
	 */
	private Element features;
	
	private Element rootElement;
	
	String sgResource;
	
	private int id = 0;
	
	private String getId() {
		id += 1;
		return "nxlca" + id;
	}

	/**
	 * Creates a new instance of the connection handler. This constructor will
	 * creating the socket, initialise the reader and writer helpers, send
	 * initial tags, authenticate the user and bind to a resource.
	 * 
	 * @param jidUser
	 *            User part of the Jabber ID.
	 * @param jidDomain
	 *            Domain part of the Jabber ID.
	 * @param resource
	 *            Resource to bind once authenticated. If null or empty, a new
	 *            resource will be generated.
	 * @param password
	 *            Password for authentication.
	 * @param session
	 *            Instance of the session to communicate with other parts of the
	 *            system.
	 * @throws XMPPException
	 *             If there is an error establishing the connection, sending or
	 *             receiving necessary data, or while authenticating.
	 */
	public XMPPConnection(String jidUser, String jidDomain, String resource,
			String password, Session session) throws XMPPException {

		this.session = session;

		initializeConnection(jidDomain);

		try {
			xmppReader = new XMPPStreamReader(socket.getInputStream());
			xmppWriter = new XMPPStreamWriter(socket.getOutputStream());
		} catch (XMPPException e) {
			throw e;
		} catch (Exception e) {
			throw new XMPPException("Could not obtain socket I/O channels ("
					+ e.getMessage() + ")", e);
		}

		initializeStreamAndFeatures(jidUser, jidDomain);

		login(jidUser, password);

		bindResource(resource);

		startListeningThread();
	}

	/**
	 * Initialises the connection with the specified domain. This method sets
	 * the socket field with an initialised socket.
	 * 
	 * @param domain
	 *            DNS name (or IP string) of the server to connect to.
	 * @throws XMPPException
	 *             If there is a problem connecting to the server.
	 */
	private void initializeConnection(String domain) throws XMPPException {

		try {
			this.socket = new Socket(domain, XMPP_DEFAULT_PORT);
		} catch (UnknownHostException e) {
			e.printStackTrace();
			throw new XMPPException("Unknown Host!");
		} catch (IOException e) {
			System.out.println(e.getMessage());
		}
	}

	/**
	 * Sends the initial data to establish an XMPP connection stream with the
	 * XMPP server. This method also retrieves the set of features from the
	 * server, saving it in a field for future use.
	 * 
	 * @param jidUser
	 *            User part of the Jabber ID.
	 * @param jidDomain
	 *            Domain part of the Jabber ID.
	 * @throws XMPPException
	 *             If there is a problem sending or receiving the data.
	 */
	private void initializeStreamAndFeatures(String jidUser, String jidDomain)
			throws XMPPException {

		this.rootElement = this.xmppWriter
				.createRootElement("stream:stream");
		rootElement.setAttribute("from", jidUser + "@" + jidDomain);
		rootElement.setAttribute("to", jidDomain);
		rootElement.setAttribute("version", "1.0");
		rootElement.setAttribute("xml:lang", "en");
		rootElement.setAttribute("xmlns", "jabber:client");
		rootElement.setAttribute("xmlns:stream",
				"http://etherx.jabber.org/streams");

		this.xmppWriter.writeRootElementWithoutClosingTag();

		Element response = this.xmppReader.readSecondLevelElement();
		this.features = response;
		this.isConnectionClosed = false;
	}
	
	private void sendStream() throws XMPPException {
		this.xmppWriter.writeRootElementWithoutClosingTag();
		//this.xmppReader.readSecondLevelElement();
		features = this.xmppReader.readSecondLevelElement();
	}

	/**
	 * Attempts to authenticate the user name with the provided password at the
	 * connected server. This method will verify if the server supports the
	 * implemented authentication mechanism(s) and send the user and password
	 * based on the first mechanism it finds. In case authentication is not
	 * successful, this function will close the connection and throw an
	 * XMPPException. This function also retrieves the new set of features
	 * available after authentication.
	 * 
	 * @param username
	 *            User name to use for authentication.
	 * @param password
	 *            Password to use for authentication.
	 * @throws XMPPException
	 *             If authentication is not successful, or if authentication
	 *             methods supported by the server are not implemented, or if
	 *             there was a problem sending authentication data.
	 */
	private void login(String username, String password) throws XMPPException {

		// LOGIN authentication method
		NodeList mechanisms = features.getElementsByTagName("mechanism");
		int inLength = mechanisms.getLength();

		Node n;
		String innerText;
		boolean isLoginSupported = false;
		for (int x = 0; x < inLength; x++) {
			n = mechanisms.item(x);
			innerText = n.getTextContent();
			if (innerText.equals("LOGIN")) {
				isLoginSupported = true;
				break;
			}
		}

		if (isLoginSupported) {
			String[] m = new String[] { "LOGIN", "PLAIN" };
			/*try {
				xmppCallbackHandler.setUserName(username);
				xmppCallbackHandler.setPassword(password);
				SaslClient sc = Sasl.createSaslClient(m, null, "xmpp",
						this.socket.getInetAddress().getCanonicalHostName(),
						null, this.xmppCallbackHandler);
			} catch (SaslException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}*/
			
			Element auth = xmppWriter.createElement("auth");
			auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
			auth.setAttribute("mechanism", "PLAIN");
			String plain = "\0" + username + "\0" + password;
			auth.setTextContent(DatatypeConverter.printBase64Binary(plain.getBytes()));
			xmppWriter.writeIndividualElement(auth);
			
			/*getChallenge();
			
			createResponse(username);
			
			getChallenge();
			
			createResponse(password);*/
			
			getChallenge();
			sendStream();
		} else {
			throw new XMPPException("Oh no LOGIN not supported!!!11");
		}
	}
	
	private void createResponse(String s) throws XMPPException {
		Element resp1 = xmppWriter.createElement("response");
		resp1.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
		
		String baseUserName = null;
		try {
			baseUserName = DatatypeConverter.printBase64Binary(s.getBytes("UTF-8"));
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		resp1.setTextContent(baseUserName);
		xmppWriter.writeIndividualElement(resp1);
	}
	
	private String getChallenge() throws XMPPException {
		Element challenge = this.xmppReader.readSecondLevelElement();
		byte[] byChallenge = DatatypeConverter.parseBase64Binary(challenge.getTextContent());
		String sgChallenge = new String(byChallenge);
		System.out.println(challenge.getNodeName());
		System.out.println(sgChallenge);
		return sgChallenge;
	}
	
	/**
	 * Binds the connection to a specific resource, or retrieves a
	 * server-generated resource if one is not provided. This function will wait
	 * until a resource is sent by the server.
	 * 
	 * @param resource
	 *            Name of the user-specified resource. If resource is null or
	 *            empty, retrieves a server-generated one.
	 * @throws XMPPException
	 *             If there is an error sending or receiving the data.
	 */
	private void bindResource(String resource) throws XMPPException {

		/* YOUR CODE HERE */
		Element iq = this.xmppWriter.createElement("iq");
		iq.setAttribute("type", "set");
		iq.setAttribute("id", "bind_1");
		Element bind = this.xmppWriter.createElement("bind");
		bind.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-bind");
		iq.appendChild(bind);
		
		if (!isNullOrEmpty(resource)) {
			Element res = this.xmppWriter.createElement("resource");
			res.setTextContent(resource);
			bind.appendChild(res);
		} else {
			this.sgResource = resource;
		}
		this.xmppWriter.writeIndividualElement(iq);
		
		Element response = this.xmppReader.readSecondLevelElement();
		boolean isOk = response.getTagName().equals("iq");
		
		Node jid = null;
		if (isOk) {
			isOk = response.getAttribute("type").equals("result");
			
			NodeList binds = response.getElementsByTagName("bind");
			
			isOk = binds.getLength() > 0;
			Node serverBind = binds.item(0);
			jid = serverBind.getFirstChild();
		}
		
		if (!isOk) {
			throw new XMPPException("Oh noes format error!");
		}
		
		String sgJid = jid.getTextContent();
		this.session.setUserJid(sgJid);
		
		int inSlashPos = sgJid.indexOf('/');
		if (inSlashPos > -1) {
			this.sgResource = sgJid.substring(inSlashPos + 1);
		}
	}
	
	private boolean isNullOrEmpty(String s) {
		if (s == null) {
			return true;
		} else {
			return s.isEmpty();
		}
	}

	/**
	 * Starts a thread that will keep listening for new messages asynchronously
	 * from the main thread.
	 */
	private void startListeningThread() {
		Thread listeningThread = new Thread(new Runnable() {

			@Override
			public void run() {
				listeningProcess();
			}
		});
		listeningThread.start();
	}

	/**
	 * Keeps listening for new XML elements in a loop until a closing tag is
	 * found or an exception happens. If an exception happens, calls
	 * <code>session.processReceivedException</code> and closes the connection.
	 * If the closing tag is found, sends a closing tag back and closes the
	 * connection as well. In both cases, the connection is closed at the
	 * session level, so that the model and UI can handle the connection being
	 * closed. For each received element, processes the element and handles it
	 * accordingly.
	 */
	private void listeningProcess() {
		String sgAttribute;
		String sgTagName;
		String sgFrom;
		Contact contact;
		
		try {
			while (!xmppReader.isDocumentComplete() && !isConnectionClosed) {
				Element e = xmppReader.readSecondLevelElement();
				if (e != null) {
					System.out.println(e.getTagName() + " " + e.getTextContent());
					
					sgTagName = e.getTagName();
					if (sgTagName.equals("iq")) {
						sgAttribute = e.getAttribute("type");
						if (sgAttribute.equals("result")) {
							handleContactRequestResult(e);
						}
					} else if (sgTagName.equals("presence")) {
						sgFrom = e.getAttribute("from");
						System.out.println(sgFrom);
						contact = session.getContact(sgFrom);
						
						if (contact != null) {
							NodeList statuses = e.getElementsByTagName("status");
							ContactStatus status;
							if (statuses.getLength() > 0) {
								try {
									status = ContactStatus.valueOf(e.getTextContent());
								} catch (IllegalArgumentException ex) {
									status = ContactStatus.OFFLINE;
								}
							} else {
								String sgType;
								sgType = e.getAttribute("type");
								if (sgType.equals("unavailable")) {
									status = ContactStatus.OFFLINE;
								} else {
									status = ContactStatus.AVAILABLE;
								}
							}
							contact.setStatus(this.sgResource, status);
						}
					} else if (sgTagName.equals("message")) {
						System.out.println(e.getTextContent());
						NodeList errors = e.getElementsByTagName("error");
						String sgMessage = "";
						if (errors.getLength() > 0) {
							Element error = (Element) errors.item(0);
							String type = error.getAttribute("code");
							if (type.equals("503")) {
								sgMessage = "Oh no! This user is offline!";
							}
						} else {
							sgMessage = e.getTextContent();
						}
							sgFrom = e.getAttribute("from");
							contact = session.getContact(sgFrom);
							if (contact != null) {
								session.getConversation(contact).addIncomingMessage(new Message(contact, null, sgMessage), sgResource);
							} else {
								System.out.println("oh noes " + sgMessage);
							}
	
					}
				}
			}
		} catch (XMPPException e) {
			this.session.processReceivedException(e);
			
			this.session.closeConnection();
			this.isConnectionClosed = true;
		} finally {

		}
	}
	
	private void handleContactRequestResult(Element iq) {
		NodeList nodeList;
		nodeList = iq.getElementsByTagName("query");
		Element query = (Element) nodeList.item(0);
		if (query != null) {
			nodeList = query.getElementsByTagName("item");
			int length = nodeList.getLength();
			if (length > 0) {
				Node n;
				Element el;
				String buddy;
				for(int x = 0; x < length; x++) {
					n = nodeList.item(x);
					if (n instanceof Element) {
						el = (Element) n;
						buddy = el.getAttribute("jid");
						if (buddy.contains("@")) {
							session.addReceivedContact(new Contact(buddy, buddy));
						}
					}
				}
			}
		}
	}

	boolean isConnectionClosed = true;
	/**
	 * Closes the connection. If the connection was already closed before this
	 * method is called nothing is done, otherwise sends all necessary closing
	 * data and waits for the server to send the closing data as well. Once this
	 * happens the socket connection is closed. This method does not throw any
	 * exception, choosing instead to ignore them. However, even if an exception
	 * happens while sending the final data, the socket connection will be
	 * closed.
	 */
	public synchronized void closeConnection() {

		if (!isConnectionClosed) {
			isConnectionClosed = true;
			try {
				this.xmppWriter.writeCloseTagRootElement();
				this.xmppReader.readSecondLevelElement();
			} catch (XMPPException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}			
			
			try {
				this.xmppReader.waitForCloseDocument();
				this.socket.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (XMPPException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	/**
	 * Sends a request for the contact list. The result is not expected to be
	 * received in this function, but it should come in a message that will be
	 * handled by the listening process.
	 * 
	 * @throws XMPPException
	 *             If there was a problem sending the request.
	 */
	public void sendRequestForContactList() throws XMPPException {

		Element iq = this.xmppWriter.createElement("iq");
		iq.setAttribute("type", "get");
		iq.setAttribute("id", this.getId());
		Element bind = this.xmppWriter.createElement("query");
		bind.setAttribute("xmlns", "jabber:iq:roster");
		iq.appendChild(bind);
		xmppWriter.writeIndividualElement(iq);
	}

	/**
	 * Sends an updated status information to the server, based on the status
	 * currently attributed to the session.
	 * 
	 * @throws XMPPException
	 *             If there was a problem sending the status.
	 */
	public void sendCurrentStatus() throws XMPPException {
		sendStatus(session.getCurrentStatus());
	}

	/**
	 * Sends a specific status information to the server.
	 * 
	 * @param status
	 *            Status to send to the server.
	 * @throws XMPPException
	 *             If there was a problem sending the status.
	 */
	private void sendStatus(ContactStatus status) throws XMPPException {
		Element el = xmppWriter.createElement("presence");
		//el.setAttribute("from", value)
		Element statusElement = xmppWriter.createElement("status");
		statusElement.setTextContent(status.getUserFriendlyName());
		String sgShow = status.getXmppShow();
		if (sgShow != null) {
			Element showElement = xmppWriter.createElement("show");
			showElement.setTextContent(sgShow);
		}
		el.appendChild(statusElement);
		xmppWriter.writeIndividualElement(el);
	}

	/**
	 * Sends a request that a new contact be added to the list of contacts.
	 * Additionally, requests authorization from that contact to receive updates
	 * any time the contact changes its status. This function does not add the
	 * user to the local list of contacts, which happens at the listening
	 * process once the server sends an update to the list of contacts as a
	 * result of this request.
	 * 
	 * @param contact
	 *            Contact that should be requested.
	 * @throws XMPPException
	 *             If there is a problem sending the request.
	 */
	public void sendNewContactRequest(Contact contact) throws XMPPException {

		Element iq = this.xmppWriter.createElement("iq");
		//Contact contactFrom = message.getFrom();
		//if (sgFrom == null) {
		String sgFrom = session.getUserJid();
		//}
		iq.setAttribute("from", sgFrom);
		iq.setAttribute("id", this.getId());
		iq.setAttribute("type", "set");
		
		Element query = this.xmppWriter.createElement("query");
		query.setAttribute("xmlns", "jabber:iq:roster");
		iq.appendChild(query);
		Element item = this.xmppWriter.createElement("item");
		item.setAttribute("jid", contact.getBareJid());
		item.setAttribute("name", contact.getAlias());
		query.appendChild(item);
		this.xmppWriter.writeIndividualElement(iq);
	}

	/**
	 * Sends a response message to a contact that requested authorization to
	 * receive updates when the local user changes its status.
	 * 
	 * @param jid
	 *            Jabber ID of the contact that requested authorization.
	 * @param accepted
	 *            <code>true</code> if the request was accepted by the user,
	 *            <code>false</code> otherwise.
	 * @throws XMPPException
	 *             If there was an error sending the response.
	 */
	public void respondContactRequest(String jid, boolean accepted)
			throws XMPPException {
		Element iq = this.xmppWriter.createElement("presence");
		iq.setAttribute("id", this.getId());
		iq.setAttribute("to", jid);
		iq.setAttribute("type", (accepted) ? "subscribed" : "unsubscribed");
	
		this.xmppWriter.writeIndividualElement(iq);
	}

	/**
		
	}

	/**
	 * Request that the server remove a specific contact from the list of
	 * contacts. Additionally, requests that no further status updates be sent
	 * regarding that contact, as well as that no further status updates about
	 * the local user be sent to that contact. This function does not remove the
	 * user from the local list of contacts, which happens at the listening
	 * process once the server sends an update to the list of contacts as a
	 * result of this request.
	 * 
	 * @param contact
	 *            Contact to be removed from the list of contacts.
	 * @throws XMPPException
	 *             If there was an error sending the request.
	 */
	public void removeAndUnsubscribeContact(Contact contact)
			throws XMPPException {

		Element iq = this.xmppWriter.createElement("iq");
		iq.setAttribute("from", this.session.getUserBareJid());
		iq.setAttribute("id", this.getId());
		iq.setAttribute("type", "set");
		
		Element query = this.xmppWriter.createElement("query");
		query.setAttribute("xmlns", "jabber:iq:roster");
		Element item = this.xmppWriter.createElement("item");
		item.setAttribute("jid", contact.getBareJid());
		item.setAttribute("subscription", "remove");
		query.appendChild(item);
		iq.appendChild(query);
	
		this.xmppWriter.writeIndividualElement(iq);
	}

	/**
	 * Send a chat message to a specific contact.
	 * 
	 * @param message
	 *            Message to be sent.
	 * @throws XMPPException
	 *             If there was a problem sending the message.
	 */
	public void sendMessage(Message message) throws XMPPException {
		Element iq = this.xmppWriter.createElement("message");
		//Contact contactFrom = message.getFrom();
		//if (sgFrom == null) {
		String sgFrom = session.getUserJid();
		//}
		iq.setAttribute("from", sgFrom);
		iq.setAttribute("to", message.getTo().getBareJid());
		
		Element body = this.xmppWriter.createElement("body");
		body.setTextContent(message.getTextMessage());
		iq.appendChild(body);
		this.xmppWriter.writeIndividualElement(iq);
	}
}
